package cn.tedu.charging.cost.service.impl;

import cn.tedu.charging.common.pojo.param.ChargingProcessParam;
import cn.tedu.charging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleRepository;
import cn.tedu.charging.cost.pojo.ChargingData;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    /**
     * 价格计算
     * A什么样的因素会影响价格
     * 1 时间段  晚上便宜 低峰 白天贵 高峰
     * 2 地理位置  站不同有可能价格不同
     * 3 枪的类型 快充 贵 慢充 便宜
     * 4 充电量
     * B 设备多次同步充电进度,多次怎么算?
     * 1 取最后一次 怎么确定是最后一次 ?
     *   1.1 判断是否是第一次或者是第N次
     *   次数 度数 价格
     *   1次 10   20
     *   2次 20   40
     *   3次 29   58
     *   4次 40   80
     * 价格的累加
     *  1次 10
     *  2次 20
     *  思考 如果你是设备的设计者 2次的度数 是否 应该包含 第一次的 ?
     *  包含
     *  不包含
     *
     *
     *
     * @param chargingProcessParam
     * @return
     */

    @Autowired
    private CostRuleRepository costRuleRepository;

    /**
     * key 订单号 value 充电数据 不同的订单号对应不同的充电数据
     */
    private Map<String, ChargingData> chargingProcessData = new HashMap<>();


    @Override
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {
        //通过chargingProcessParam入参订单号 获取充电数据 从map里获取充电数据,有就返回,没有就初始化
        ChargingData chargingData = buildChargingData(chargingProcessParam);
        //获取场站信息
        Integer stationId = chargingProcessParam.getStationId();
        //当前时间
        LocalDateTime now = LocalDateTime.now();
        //当前小时
        int hour = now.getHour();
        //通过场站信息和时间获取获取计价规则
        CostRulePO costRule = getMatchCostRuleByTime(stationId,hour);
        log.debug("通过场站信息:{},充电同步时间:{},获取计价规则{}",stationId,hour,costRule);
        if (costRule != null) {
            //计价规则名称
            String name = costRule.getName();
            //每度电的价格
            BigDecimal powerFee = costRule.getPowerFee();
            //当前时间段的单价保存到chargingData
            chargingData.setCurrentOneFee(powerFee);
            log.debug("当前时间(小时):{},计价规则名称:{},电费单价:{}",hour,name,powerFee);
            //判断是不是第一次
            if (chargingData.getLastChargingProcess() == null) {
                appendFirstCost2ChargingData(chargingProcessParam,chargingData,powerFee, hour, name);
            }else {
                appendLastCost2ChargingData(chargingProcessParam,chargingData, powerFee, hour, name);
            }
        }else { //计价规则表里基本都是站点5，所以小刀/mqttfx测试时，如果发送的站点为1，就会计价错误，所以加个else告警
            //如果没有获取到计价规则,需要告警,接入监控系统,停止充电了.返回空对象
            log.error("获取计价规则失败,告警告警告警");   //也可以用自己的全局异常处理器捕捉
            return null;//或return new ChargingProcessVO();然后在MqttConsumer里handleChargingProcess也要处理计价为0的情况
        }
        //记录最新的充电总度数
        chargingData.setTotalCapacity(chargingProcessParam.getChargingCapacity());
        return buildChargingProcessVo(chargingData);
    }

    /**
     * 通过当前同步的充电数据的时间和站信息 获取匹配的计价规则
     * @param stationId
     * @return
     */
    private CostRulePO getMatchCostRuleByTime(Integer stationId,int hour){
        //获取设备的站id获取计价规则
        log.debug("通过站id获取计价规则:入参:{}",stationId);
        List<CostRulePO> costRules = getCostRule(stationId);
        log.debug("通过站id获取计价规则:入参:{},出参:{}",stationId,costRules);

        log.debug("获取当前时间 小时 :{} 通过当前小时 来计算 当前小时 属于 哪个时间段 尖 峰 平 谷 计算价格",hour);
        //循环当前计价列表
        for (CostRulePO costRule : costRules) {
            //判断当前小时属于哪个时间段
            //当前时间 大于等于计价规则的开始时间 并且 小于 结束时间
            if (hour >= costRule.getStartTime() && hour < costRule.getEndTime()) {
                return costRule;
            }
        }
        return null;
    }


    /**
     * 通过订单号去 map中获取充电记录,
     * 有 返回
     * 没有 构建新的
     * @return
     */
    private ChargingData buildChargingData(ChargingProcessParam chargingProcessParam){
        //获取订单号
        String orderNo = chargingProcessParam.getOrderNo();
        //从map获取充电记录
        ChargingData chargingData = chargingProcessData.get(orderNo);
        //判断充电记录是否为空
        if (chargingData == null) {
            //如果为空,初始化充电记录,并且把充电记录存入map
            chargingData = initChargingData(chargingProcessParam);
        }
        return chargingData;
    }

    /**
     * 初始化充电数据 订单和设备的基本信息可以在这里初始化,设备的动态信息 比如充电度数,比如温度不能在这初始化
     * 因为这的代码 同一个订单号,只执行一次
     * @param chargingProcessParam
     * @return
     */
    private ChargingData initChargingData(ChargingProcessParam chargingProcessParam) {
        String orderNo = chargingProcessParam.getOrderNo();
        log.debug("通过订单号:{},没有取到充电数据,表示第一次同步充电数据",orderNo);
        //第一次 初始化充电数据
        ChargingData chargingData = new ChargingData();
        //设置订单号
        chargingData.setOrderNo(orderNo);
        //设置实际充电开始时间和订单创建时间是有点小区别的
        chargingData.setStartTime(LocalDateTime.now());
        //设置枪id
        chargingData.setGunId(chargingProcessParam.getGunId());
        //设置用户id    13.1添加，给用户推送的充电消息需要
        chargingData.setUserId(chargingProcessParam.getUserId());
        //保存初始化的充电数据
        chargingProcessData.put(orderNo,chargingData);
        return chargingData;
    }

    /**
     * 计算不是第一充电的数据 需要累加价格
     * @param chargingProcessParam
     * @param chargingData
     * @param powerFee
     * @param hour
     * @param name
     */
    private void appendLastCost2ChargingData(ChargingProcessParam chargingProcessParam, ChargingData chargingData, BigDecimal powerFee, int hour, String name) {
        //不是第一次
        log.debug("获取上一次充电状态记录,取到 ,表示不是第一次同步充电数据");
        //做累加
        // 1:3平 2:1尖 3:2峰  每次同步的是 实际冲了多少度电 需要记录每次充了多少 3 + 1 + 2 = 6 不包含
        // 1:3平 2:4尖 3:6峰  每次同步的是 已经充了多少度  只需记录已经充了多少  取最后一次 6    包含
        // [(3*平的价格)] + [(4-3) * 尖的价格] + [(6-4) * 峰的价格]
        float realChargingCapacity = getRealChargingCapacity(chargingProcessParam, chargingData);
        // 本次花费 = 每度电的价格 乘以 充电度数
        BigDecimal currentChargingCost = getCost(realChargingCapacity, powerFee);
        log.debug("当前时间(小时):{},计价规则名称:{},电费单价:{},充电度数:{},本次花费:{}",
                hour, name, powerFee,realChargingCapacity,currentChargingCost);
        //获取上次的花费
        BigDecimal totalCost = chargingData.getTotalCost();
        //累加 当前总花费到 = 上次花费 + 本次花费
        BigDecimal currentTotalCost = totalCost.add(currentChargingCost);
        //保存当前总花费到 chargingData
        chargingData.setTotalCost(currentTotalCost);
        log.debug("当前时间(小时):{},计价规则名称:{},电费单价:{},充电度数:{},本次花费:{},总花费:{}",
                hour, name, powerFee,realChargingCapacity,currentChargingCost,currentTotalCost);
        //保存当前充电记录到chargingData,为下次计算做准备
        chargingData.setLastChargingProcess(chargingProcessParam);
    }

    /**
     * 计算第一次充电的数据
     * @param chargingProcessParam
     * @param chargingData
     * @param powerFee
     * @param hour
     * @param name
     */
    private void appendFirstCost2ChargingData(ChargingProcessParam chargingProcessParam, ChargingData chargingData, BigDecimal powerFee, int hour, String name) {
        log.debug("获取上一次充电状态记录,没取到 ,表示第一次同步充电数据");
        //第一上传充电进度
        //获取充电度数
        float chargingCapacity = chargingProcessParam.getChargingCapacity();
        //本次花费 = 每度电的价格 乘以 充电度数
        BigDecimal chargingCost =  getCost(chargingCapacity, powerFee);
        log.debug("当前时间(小时):{},计价规则名称:{},电费单价:{},充电度数:{},本次花费:{}",
                hour, name, powerFee,chargingCapacity,chargingCost);
        //保存当前总花费到 chargingData
        chargingData.setTotalCost(chargingCost);
        //保存当前充电记录到chargingData,为下次计算做准备
        chargingData.setLastChargingProcess(chargingProcessParam);
    }

    /**
     *  获取实际的充电度数
     *  当前充电度数 - 上次充电度数
     * @param chargingProcessParam  当前同步数据的入参 包含 当前充电度数
     * @param chargingData 上传同步数据的记录 包含 上传充电的度数
     * @return
     */
    private float getRealChargingCapacity(ChargingProcessParam chargingProcessParam,ChargingData chargingData) {
        //取上次充电记录
        ChargingProcessParam lastChargingProcess = chargingData.getLastChargingProcess();
        //取上次充电的度数
        float lastChargingCapacity = lastChargingProcess.getChargingCapacity();
        //取最新充电度数
        float currentChargingCapacity = chargingProcessParam.getChargingCapacity();
        //当前实际充电度数
        float realChargingCapacity = currentChargingCapacity - lastChargingCapacity;
        return realChargingCapacity;
    }

    /**
     * 计算价格 度数 * 单价
     * @param chargingCapacity  度数
     * @param powerFee  单价
     * @return
     */
    private BigDecimal getCost(float chargingCapacity,BigDecimal powerFee){
        //把float类型的充电度数 chargingCapacity 转换为 BigDecimal 类型的 chargingCapacityBigDecimal;
        BigDecimal chargingCapacityBigDecimal = new BigDecimal(Float.toString(chargingCapacity));
        //花费 = 度数 * 单价
        return powerFee.multiply(chargingCapacityBigDecimal);
    }

    /**
     * 构建充电进度返回结果
     * @return
     */
    private ChargingProcessVO buildChargingProcessVo(ChargingData chargingData){
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();
        //总花费
        chargingProcessVO.setTotalCost(chargingData.getTotalCost());
        //订单编号
        chargingProcessVO.setOrderNo(chargingData.getOrderNo());
        //枪id
        chargingProcessVO.setGunId(chargingData.getGunId());
        //用户id  13.1添加，给用户推送的充电消息需要
        chargingProcessVO.setUserId(chargingData.getUserId());
        //chargingData 里的价格是当前的价格 充电要跨越多个时间段 价格 = 总费用 / 总度数 todo
        chargingProcessVO.setPowerFee(chargingData.getCurrentOneFee());
        //总度数
        chargingProcessVO.setTotalCapacity(chargingData.getTotalCapacity());
        //总时长 当前时间 - 充电开始时间
        Duration between = Duration.between(chargingData.getStartTime(), LocalDateTime.now());
        String totalTime = between.toDays() + "-" + between.toHours() + "-" + between.toMinutes() + "-" + between.toMillis();
        chargingProcessVO.setTotalTime(totalTime);
        return chargingProcessVO;
    }

    @Autowired  //13.4 储存到redis
    private CostRuleCacheRepository costRuleCacheRepository;
    /**
     * 通过站id获取计价列表
     * @param stationId
     * @return
     */
    private List<CostRulePO> getCostRule(Integer stationId) {
        //return costRuleRepository.getCostRuleByStationId(stationId);  13.4就不直接从数据库查了

        //先从redis获取 13.4
        log.debug("通过站点id:{}从redis获取计价规则",stationId);
        List<CostRulePO> cacheCostRules = costRuleCacheRepository.getCostRuleByStationId(stationId);
        //判断redis是否有数据
        //if (cacheCostRules != null ){
        if(!CollectionUtils.isEmpty(cacheCostRules)){
            //redis有数据 返回
            log.debug("通过站点id:{}从redis获取计价规则,redis有数据 直接返回",stationId);
            return cacheCostRules;
        }else {
            //如果redis没有数据 从数据库查询
            log.debug("通过站点id:{}从 Redis没有查询到数据 从数据库查询",stationId);
            List<CostRulePO> dbCostRules = costRuleRepository.getCostRuleByStationId(stationId);
            //判断数据库是否有数据
            if(!CollectionUtils.isEmpty(dbCostRules)){
                //数据库有数据 给cost业务提供数据前，先写入redis
                log.debug("通过站点id:{} 数据库有数据 写入到Redis,并返回",stationId);
                costRuleCacheRepository.saveCostRule(stationId,dbCostRules);
                return dbCostRules;
            }
        }
        return Collections.emptyList();//就是return null;但上面的getMatchCostRuleByTime里的for循环遍历，null会报错，空list则不报错
        //写完之后重启测试，小刀cost发送桩反馈数据，redis中get COST_RULE_KEY_5能看到json格式储存的计价规则，不过是序列化后的？
    }

}
